package view;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import util.coordTranslate.AbstractCoordinateSystem;
import util.coordTranslate.CartesianCoordinateSystem;
import util.resources.ResourceManager;
import exceptions.ResizingException;

/**
 * Represents a turtle's environment.
 * 
 * Only the following formats are supported: png, jpg, gif.
 * 
 * @author Robert C. Duvall
 * @author Anne Weng and Nathan Klug
 */
public class Pixmap
{
    private static ResourceManager resources = ResourceManager.getInstance();

    private static Dimension getDefaultSize()
    {
        return resources.getDimension("Pixmap_size");
    }

    private static Color getDefaultColor()
    {
        return resources.getColor("Pixmap_color");
    }

    private static final String DEFAULT_FILENAME = resources
            .getString("Pixmap_name");
    private static final String DEFAULT_IMAGE_TYPE = resources
            .getString("Pixmap_imageType");

    private AbstractCoordinateSystem getDefaultCoordinates(Dimension size)
    {
        return new CartesianCoordinateSystem(size);
    }

    private Camera myCamera;
    private String myFileName;
    private BufferedImage myImage;
    private Color myBackgroundColor;
    private AbstractCoordinateSystem myCoordinates;

    /**
     * Create a default pixmap
     */
    public Pixmap()
    {
        this(getDefaultSize());
    }

    /**
     * Create a default pixmap with given size
     * 
     * @param size
     */
    public Pixmap(Dimension size)
    {
        this(size.width, size.height, getDefaultColor());
    }

    /**
     * Create a default pixmap with given width and height
     * 
     * @param width
     *            , height
     */
    public Pixmap(int width, int height)
    {
        this(width, height, getDefaultColor());
    }

    /**
     * Create a pixmap with given width and height and filled with given initial
     * color
     * 
     * @param width
     *            , height, color
     */
    public Pixmap(int width, int height, Color color)
    {
        myBackgroundColor = color;
        myImage = createImage(width, height, color);
        myFileName = DEFAULT_FILENAME;
        myCoordinates = getDefaultCoordinates(getSize());

    }

    /**
     * Create a pixmap as a copy of the given pixmap
     * 
     * @param other
     */
    public Pixmap(Pixmap other)
    {
        myFileName = other.myFileName;
        myImage = copyImage(getSize(), getSize(), other.myImage);
        myCoordinates = getDefaultCoordinates(getSize());
    }

    /**
     * Create a pixmap as a copy of the given BufferedImage
     * 
     * @param image
     */
    public Pixmap(BufferedImage image)
    {
        myFileName = DEFAULT_FILENAME;
        myImage = image;
        myCoordinates = getDefaultCoordinates(getSize());
    }

    /**
     * Create a pixmap from the given local file
     * 
     * @param fileName
     * @throws IOException
     */
    public Pixmap(String fileName) throws IOException
    {
        if (fileName == null)
        {
            createImage(getDefaultSize().width, getDefaultSize().height,
                    getDefaultColor());
        }
        else
        {
            read(fileName);
        }
        myCoordinates = getDefaultCoordinates(getSize());
    }

    /**
     * @return name of this Pixmap (without file system information)
     * 
     */
    public String getName()
    {
        int index = myFileName.lastIndexOf(File.separator);
        if (index >= 0)
            return myFileName.substring(index + 1);
        else
            return myFileName;
    }

    /**
     * @return true iff the given coordinate is within the bounds of this Pixmap
     */
    public boolean isInBounds(int x, int y)
    {
        return (0 <= x && x < getSize().width)
                && (0 <= y && y < getSize().height);
    }

    /**
     * @return bounds of this Pixmap
     */
    public Dimension getSize()
    {
        return new Dimension(myImage.getWidth(), myImage.getHeight());
    }

    public void setCamera(Camera camera)
    {
        myCamera = camera;
    }

    public void containerSizeChanged(Dimension size)
    {
        resizeToFit(size);
        myCamera.changeDisplaySize(size);
    }

    /**
     * Changes the size of this Pixmap
     * 
     * Adds default color to fill in blank space if necessary
     * 
     * @param size
     * @return void
     */
    public void resizeToFit(Dimension size)
    {
        resizeToFit(size.width, size.height);
    }

    /**
     * Changes the size of this Pixmap
     * 
     * Adds default color to fill in blank space if necessary
     * 
     * @param width
     *            , height
     * @return void
     */
    public void resizeToFit(int width, int height)
    {
        if (width != getSize().width || height != getSize().height)
        {
            if ((width > 0) && (height > 0))
            {
                Dimension newSize = new Dimension(width, height);
                if (width > getSize().width || height > getSize().height)
                {
                    myImage = copyImage(getSize(), newSize, myImage);
                }
                /*
                 * else This shouldn't be in here, because it causes a loss of
                 * data { myImage = myImage.getSubimage(0, 0, width, height); }
                 */
            }
            // This handles the case of a negative resizing, which shouldn't
            // happen,
            // but has happened in the past, by taking the absolute value
            else
            {
                int oldWidth = width;
                int oldHeight = height;
                resizeToFit(Math.abs(width), Math.abs(height));
                throw new ResizingException(oldWidth, oldHeight);
            }

        }
    }

    /**
     * Returns this Pixmap as as an Icon to be used in swing.
     * 
     * @return Icon
     */
    public Icon toIcon()
    {
        return new ImageIcon(myImage);
    }

    /**
     * Returns this Pixmap as as an Icon to be used in swing.
     * 
     * @param width
     *            , height
     * @return Icon
     */
    public Icon toIcon(int width, int height)
    {
        return new ImageIcon(myImage.getScaledInstance(width, height,
                Image.SCALE_SMOOTH));
    }

    /**
     * Updates this Pixmap to reflect colors from the given local file
     * 
     * Note, changes this Pixmap's size if necessary
     * 
     * @param fileName
     * @return void
     * @throws IOException
     */
    public void read(String fileName) throws IOException
    {
        myFileName = fileName;
        myImage = ImageIO.read(new File(myFileName));
    }

    /**
     * Saves this Pixmap to the given local file as a JPEG image
     * 
     * @param fileName
     * @return void
     * @throws IOException
     */
    public void write(String fileName) throws IOException
    {
        ImageIO.write(myImage, DEFAULT_IMAGE_TYPE, new File(fileName));
    }

    /**
     * Creates a blank image of the given color with the given dimensions
     * 
     * @param width
     * @param height
     * @param color
     */
    private BufferedImage createImage(int width, int height, Color color)
    {
        BufferedImage tempImage = new BufferedImage(width, height,
                BufferedImage.TYPE_INT_RGB);
        setImageToColor(tempImage, color);
        return tempImage;
    }

    /**
     * Returns a resized (by truncation/extension, not scaling) version of a
     * given bufferedImage
     * 
     * @param from
     * @param to
     * @param original
     */
    private BufferedImage copyImage(Dimension from, Dimension to, BufferedImage original)
    {
        int[] data = new int[from.width * from.height];
        BufferedImage result = new BufferedImage(to.width, to.height,
                BufferedImage.TYPE_INT_RGB);
        setImageToColor(result, myBackgroundColor);
        original.getRGB(0, 0, from.width, from.height, data, 0, from.width);
        result.setRGB(0, 0, Math.min(from.width, to.width),
                Math.min(from.height, to.height), data, 0, from.width);
        return result;
    }

    public void setImageBackground(BufferedImage image)
    {
        Graphics2D imageg2d = image.createGraphics();
        imageg2d.fillRect(0, 0, image.getWidth(), image.getHeight());
    }

    public void clearPixmap()
    {
        setImageToColor(myImage, myBackgroundColor);
    }

    /**
     * Sets the given image to a blank image of the given color
     * 
     * @param image
     * @param color
     */
    private void setImageToColor(BufferedImage image, Color color)
    {
        Graphics2D imageg2d = image.createGraphics();
        imageg2d.setPaint(color);
        imageg2d.fillRect(0, 0, image.getWidth(), image.getHeight());
    }

    /**
     * Should only be used to get copy of the image for things like the history.
     * Should not be used in conjunction with "setImage" to draw un-safely.
     * 
     * @return the current buffered image
     */
    public BufferedImage getImageCopy()
    {
        BufferedImage copy = copyImage(getSize(), getSize(), myImage);
        return copy;
    }

    /**
     * Sets the Pixmap's image to be the given image.
     * 
     * TODO: This is a bit unsafe. Is there a more secure way to do this?
     * 
     * @param image
     */
    public void setImage(BufferedImage image)
    {
        myImage = image;
    }

    /**
     * Sets the properties of the graphics object of the image to those
     * specified by the pen, then draws the shape. If the shape extends beyond
     * the boundaries of the pixmap, the pixmap is resized, so information is
     * not lost.
     * 
     * @param pen
     * @param shape
     * @param lineWidth
     */

    public void setPropertiesAndDraw(Pen pen, Shape shape, Float lineWidth)
    {
        resizeToShape(shape);

        Graphics2D g2 = (Graphics2D) pen.setDrawProperties(myImage
                .getGraphics());
        g2.setStroke(new BasicStroke(lineWidth));

        drawShape(shape, g2);
    }

    /**
     * Resizes the pixmap so the given shape fits
     * 
     * @param shape
     */
    private void resizeToShape(Shape shape)
    {
        Rectangle shapeBounds = shape.getBounds();
        Dimension newSize = new Dimension();
        newSize.width = Math.max(shapeBounds.x + shapeBounds.width,
                getSize().width);
        newSize.height = Math.max(shapeBounds.y + shapeBounds.height,
                getSize().height);
        setImage(copyImage(getSize(), newSize, myImage));
    }

    /**
     * Draws the given shape
     * 
     * @param s
     */
    private void drawShape(Shape s, Graphics2D g)
    {
        g.draw(s);
    }

    public void tellCameraToDraw(Graphics g)
    {
        myCamera.drawView(g);
    }

    /**
     * Draws the given area
     * 
     * @param g
     * @param topLeft
     * @param viewSize
     */
    public void drawFromCamera(Graphics g, Point topLeft, Dimension viewSize, Dimension displaySize)
    {
        BufferedImage imageToDraw = copyImage(getSize(), viewSize, myImage);
        g.drawImage(imageToDraw, topLeft.x, topLeft.y, displaySize.width,
                displaySize.height, null);
    }

    public AbstractCoordinateSystem getCoordinates()
    {
        return myCoordinates;
    }

    public void setCoordinates(AbstractCoordinateSystem coordinates)
    {
        myCoordinates = coordinates;
    }

    public Point translateCoordinates(Point point)
    {
        return myCoordinates.translateCoordinates(point);
    }

    public Point translateCoordinates(double prevX, double prevY)
    {
        return myCoordinates.translateCoordinates(prevX, prevY);
    }

    public void zoom(double zoomFactor)
    {
        myCamera.zoom(zoomFactor);
    }

    public void moveCamera(int left, int up)
    {
        myCamera.move(left, up);
        resizeToShape(myCamera.getCurrentViewRectangle());
    }

}
